package sg.edu.nus.iss.jass.datastore.impl;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import sg.edu.nus.iss.jass.datastore.ISelector;

/**
 * <p>An implementation of {@link ISelector}.</p>
 * <li>The type of object in the list can't be primitive type or wrapper class of primitive type,</li> 
 * <li>The object must be standard POJO bean</li> 
 * <li>The attribute, is to enroll in comparison, must implement interface <code>java.lang.Compariable</code>.</li> 
 * </br><b>Code example:</b></br> 
 * <code>
 * 	class User {</br>
 * 		<ul>private String name;</br>
 * 		private int age;</br>
 * 		public User(String name, int age){</br>
 * 		}</br>
 * 		<b>//ignore getter/setter method</b></br>
 * 	}
 * </code></br> 
 * 		<b>//Suppose have a list:</b></br> <code>
 * 		List<User> list = new ArrayList<User></br>
 * 		User user1 = new User("A",10);</br>
 * 		User user2 = new User("A",12);</br>
 * 		User user3 = new User("B",9);</br>
 * 		...</br>
 * 		User usern = new User("C",n);</br>
 * 		list.add(user1)</br>
 * 		...</br>
 * 		list.add(usern)</br>
 * </code>
 * 
 * <b>//Now if you want to get all the user with name is 'A' and age is between 10-20</b></br> 
 * <code>
 * 		Selector selector = new Selector();</br>
 * 		<b>// type of attribute name String implemented Comparable</b></br>
 * 		<b>// type of attribute age int is Comparable</b></br>
 * 		List<User> newList = selector.addEquals("name","A")</br>
 * 									.addGreatEqual("age",10)</br>
 * 									.addLessEqual("age",20)</br>
 * 									.filter(list);</br>
 * </ul>
 * </code>
 * <br/>
 * 
 * <b>The following comparison/ logic operation and any combination of these comparison is supported:</b>
 * <ul>
 * <i>eq - equal to</i></br>
 * <i>gt - greater than</i></br>
 * <i>lt - less than</i></br>
 * <i>ge - greater or equal to</i></br>
 * <i>le - less or equal to</i></br>
 * <i>ne - not equal to</i></br>
 * <i>in - equal to any in the given list</i></br>
 * <i>notin - not equal to any in the given list</i></br>
 * <i>or - logic or operation</i></br>
 * <i>the default link of comparator - logic and</i>
 * </ul>
 * @author WANG DAYONG
 * 
 */
public class Selector implements ISelector {
	
	/**
	 * Predefined evaluator type
	 * 
	 * @author WANG DAYONG
	 *
	 */
	private enum EvalType {
		gt, lt, eq, le, ge, ne, in, notin, like, notlike, regex
	}

	private List<Evaluator> evaluators = new ArrayList<Evaluator>();

	private List<Selector> logicOrs = null;

	/**
	 * Add equal to condition
	 * 
	 * @param field
	 *            the attribute name
	 * @param value
	 *            excepted value
	 * @return
	 * 			Selector with 'eq' filter
	 */
	public Selector eq(String field, Object value) {

		evaluators.add(new Evaluator(EvalType.eq, field, value));
		return this;
	}

	/**
	 * Add greater than to condition
	 * 
	 * @param field
	 *            the field name
	 * @param value
	 *            excepted value
	 * @return
	 * 			Selector with 'gt' filter
	 */
	public Selector gt(String field, Object value) {

		evaluators.add(new Evaluator(EvalType.gt, field, value));
		return this;
	}

	/**
	 * Add less than to condition
	 * 
	 * @param field
	 *            the field name
	 * @param value
	 *            excepted value
	 * @return
	 * 			Selector with 'lt' filter
	 */
	public Selector lt(String field, Object value) {

		evaluators.add(new Evaluator(EvalType.lt, field, value));
		return this;
	}

	/**
	 * Add greater equal to condition
	 * 
	 * @param field
	 *            the field name
	 * @param value
	 *            excepted value
	 * @return
	 * 			Selector with 'ge' filter
	 */
	public Selector ge(String field, Object value) {

		evaluators.add(new Evaluator(EvalType.ge, field, value));
		return this;
	}

	/**
	 * Add less equal to condition
	 * 
	 * @param field
	 *            the field name
	 * @param value
	 *            excepted value
	 * @return
	 * 			Selector with 'le' filter
	 */
	public Selector le(String field, Object value) {

		evaluators.add(new Evaluator(EvalType.le, field, value));
		return this;
	}

	/**
	 * Add not equal to condition
	 * 
	 * @param field
	 *            the field name
	 * @return
	 * 			Selector with 'ne' filter
	 */
	public Selector ne(String field, Object value) {

		evaluators.add(new Evaluator(EvalType.ne, field, value));
		return this;
	}

	/**
	 * Add in condition
	 * 
	 * @param field
	 *            the field name
	 * @return
	 * 			Selector with 'in' filter
	 */
	public Selector in(String field, List<Object> values) {

		evaluators.add(new Evaluator(EvalType.in, field, values));
		return this;
	}
	
	/**
	 * Add in condition
	 * 
	 * @param field
	 *            the field name
	 * @return
	 * 			Selector with 'like' filter
	 */
	public Selector like(String field, String value) {

		evaluators.add(new Evaluator(EvalType.like, field, value));
		return this;
	}
	
	/**
	 * Add in condition
	 * 
	 * @param field
	 *            the field name
	 * @return
	 * 			Selector with 'notlike' filter
	 */
	public Selector notlike(String field, String value) {

		evaluators.add(new Evaluator(EvalType.notlike, field, value));
		return this;
	}

	/**
	 * Add not in condition
	 * 
	 * @param field
	 *            the field name
	 * @return
	 * 			Selector with 'noin' filter
	 */
	public Selector notin(String field, List<Object> values) {

		evaluators.add(new Evaluator(EvalType.notin, field, values));
		return this;
	}
	
	/**
	 * Add Regular Expressions in condition
	 * 
	 * @param field
	 *            the field name
	 * @return
	 * 			Selector with 'Regular Expressions' filter
	 */
	public Selector regex(String field, String regex) {

		evaluators.add(new Evaluator(EvalType.regex, field, regex));
		return this;
	}

	/**
	 * Add logic operation or
	 * 
	 * @return
	 * 			Selector with logic operation 'or'
	 */
	public Selector or(Selector selector) {

		if (logicOrs==null) {
			logicOrs = new ArrayList<Selector>();
		}
		logicOrs.add(selector);
		return this;
	}

	/**
	 * <p>Filter objects from given collection.</p>
	 * Objects contains in collection must be same type, and must be POJO bean.
	 * 
	 * @param <T> Any object which is not primitive type
	 * @param list The target collection to be tested.
	 * @return List Expected objects after testing
	 * @throws Exception
	 * 			If encountering error when performing filtering
	 */
	public <T extends Object> List<T> filter(Collection<T> list) throws Exception {

		if (list!=null) {
			List<T> newList = new ArrayList<T>();
			for (T obj : list) {
				boolean isSatisified = true;

				// check the first level evaluators
				for (Evaluator eval : evaluators) {
					if (!eval.isTrue(obj)) {
						isSatisified = false;
						break;
					}
				}

				// if meet the condition, add obj to result list
				if (isSatisified) {
					newList.add(obj);
				} else {
					// if still have logic or
					if (logicOrs!=null) {
						// do evaluation using inner selector
						for (Selector orSelector : logicOrs) {
							if (orSelector.isSatisfiied(obj)) {
								newList.add(obj);
							}
						}
					}
				}

			}
			return newList;
		}
		return null;
	}

	/**
	 * <p>Test whether the object satisfied given evaluators</p>
	 * 
	 * @param obj Object to be evaluated
	 * @return <code>true</code>, if pass all the testing, else return <code>false</code>
	 * @throws Exception
	 * 		If encountering error while performing testing
	 */
	private boolean isSatisfiied(Object obj) throws Exception {

		boolean isSatisified = true;
		for (Evaluator eval : this.evaluators) {
			if (!eval.isTrue(obj)) {
				isSatisified = false;
				break;
			}
		}
		return isSatisified;
	}



	/**
	 * <p>The container to store filter condition</p>
	 * 
	 * @author WANG DAYONG
	 *
	 */
	private class Evaluator {

		/**
		 * name of attribute to be evaluated
		 */
		String filed = null;

		/**
		 * the expect value
		 */
		Object value = null;

		/**
		 * Evaluation type, see {@link EvalType}
		 */
		EvalType type = null;

		/**
		 * Constructor
		 * @param type	Evaluator type
		 * @param field The attribute name is to be evaluated
		 * @param value The except
		 */
		Evaluator(EvalType type, String field, Object value) {

			this.type = type;
			this.filed = field;
			this.value = value;
		}

		/**
		 * <p>Test object against evaluators</p>
		 * @param origObj	The object is to be evaluated
		 * @return
		 * 		If the given object is as expected, return <code>true</code>, else return <code>false</code>.
		 * @throws Exception
		 * 		If encountering any error while performing testing.
		 */
		boolean isTrue(Object origObj) throws Exception {

			boolean result = false;

			Comparable origValue = (Comparable) getValue(origObj, filed);

			if (origValue==null&&value!=null) {
				return false;
			}

			if (origValue==null&&value==null) {
				return true;
			}

			switch (type) {
				case eq:
					result = origValue.equals(value);
					break;
				case ne:
					result = !origValue.equals(value);
					break;
				case lt:
					result = (origValue.compareTo(value)<0) ? true : false;
					break;
				case le:
					result = (origValue.compareTo(value)<=0) ? true : false;
					break;
				case ge:
					result = (origValue.compareTo(value)>=0) ? true : false;
					break;
				case gt:
					result = (origValue.compareTo(value)>0) ? true : false;
					break;
				case in:
					List values = (List) value;
					result = values.contains(origValue);
					break;
				case notin:
					values = (List) value;
					result = !values.contains(origValue);
					break;
				case regex:
					if (origValue instanceof String) {
						if(String.valueOf(origValue).matches(String.valueOf(value))){
							result = true;
						}
					}
					break;
				case like:
					if (origValue instanceof String) {
						if(String.valueOf(origValue).toLowerCase().contains(String.valueOf(value).toLowerCase())){
							result = true;
						}
					} else {
						result = false;
					}
					break;
				case notlike:
					if (origValue instanceof String) {
						if(String.valueOf(origValue).toLowerCase().contains(String.valueOf(value).toLowerCase())){
							result = false;
						} else {
							result = true;
						}
					} else {
						result = true;
					}
					break;
			}

			return result;
		}

		/**
		 * <p>Get the value of the specified field in given object</p>
		 * 
		 * @param obj The target object.
		 * @param field The target filed name.
		 * @return The value of the filed of given object
		 * @throws Exception
		 * 					If encountering error while performing reflection
		 */
		private Object getValue(Object obj, String field) throws Exception {

			String methodName = "get"+field.substring(0, 1).toUpperCase()+field.substring(1, field.length());
			try {
				Method method = obj.getClass().getMethod(methodName, null);
				if (method==null) {
					return null;
				} else {
					return method.invoke(obj, null);
				}
			} catch (SecurityException e) {
				throw e;
			} catch (NoSuchMethodException e) {
				throw e;
			} catch (IllegalArgumentException e) {
				throw e;
			} catch (IllegalAccessException e) {
				throw e;
			} catch (InvocationTargetException e) {
				throw e;
			}
		}

	}
}
